[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータをオブジェクト検出で利用可能なRecordIO形式に変換してみました
1 はじめに
CX事業本部の平内(SIN)です。
Amazon SageMaker の組み込みの物体検出アルゴリズム(object-detection)では、RecordIO形式のデータセットが利用可能です。
今回は、Amazon SageMaker Ground Truth(以下、Ground Truth)で作成したデータを変換して、このRecordIO形式のデータセットを作成してみました。
実は、物体検出アルゴリズム(object-detection)では、RecordIO形式の他にも、イメージ形式、拡張形式という3種類のデータセットが利用可能であり、拡張形式は、Ground Truthの出力を指しているので、わざわざ変換しなくても利用可能です。(fit()の際に、RecordIO形式に自動的に変換されている模様)
しかし、増加学習は、イメージ形式と、RecordIO形式でしか行うことが出来ない事と、学習のスピードは、イメージ形式よりRecordIOの方が高速でオススメとの事なので、今回、RecordIO形式への変換作業も確認しておくことにしました。
2 Ground Truth
Ground Truthのデータセットは、前回作成した、プライベートプロジェクトのごく少数のものです。
[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータをオブジェクト検出で利用可能なイメージ形式に変換してみました
作業が完了すると、各画像ごと1行のアノテーション情報となっているoutput.manifestが生成されますが、このファイルと画像ファイルが今回の変換元です。
3 RecordIO形式のデータ
RecordIO形式のデータは、MXNetで配布される im2rec.py を使用して作成することができます。
im2rec.pyでは、アノテーション情報として.lst形式の入力を必要とするため、Ground Truthの出力であるoutput.manifestから生成しました。
以下は、生成した.lstファイルと、その当該画像の一例です。
1 4 5 800 600 1 0.141 0.225 0.454 0.695 0 0.534 0.392 0.772 0.632 AHIRU-1586397390447333.jpg (略) 9 4 5 800 600 1 0.352 0.363 0.614 0.805 AHIRU-1586397276514928.jpg (略)
.lstファイルの構造は、以下のとおりです。
- 連番 (1から始まる連番)
- ヘッダ数 (ヘッダを構成するカラム数 「ヘッダ数、データ数、画像サイズ幅、画像サイズ高さ」で4となる)
- データ数(1つのアノテーションデータを構成するカラム数「ラベル、左上X座標、左上Y座標、右下X座標、右下Y座標」で5となる)
- 画像サイズ(幅)
- 画像サイズ(高さ)
- 1つ目アノテーションデータ(ラベル、左上X座標、左上Y座標、右下X座標、右下Y座標)座標は、0.0〜1.0で表現される
- 2つ目のアノテーションデータ (2つ目以降がある場合は、順番に配置される)
- 画像ファイル名 (画像ファイル名、相対的なパスで指定される)
参考:https://github.com/leocvml/mxnet-im2rec_tutorial
im2rec.pyの使用例です。your_image_folderには、画像が配置されているディレクトリが指定されます。
$ python3 im2rec.py --pack-label
4 学習データと検証データ
教師あり学習のアルゴリズムである、オブジェクト検出では、学習用のデータと、検証用のデータが必要です。
学習用と検証用として、元データを、一定の比率で分割する場合、単純に分割することができません。全てのデータに均等に全てのラベルが含まれているわけでは無いからです。
たとえば、先頭から40件目まで、Aというラベルが指定されており、41〜50件目まで、Bが指定されているとします。そして、これを単純に4:1に分割すると、1〜40と41〜50となるため、学習用データにBのデータが存在しないことになってしまいます。
この問題に対応するため、変換対象となる全てのデータから、含まれるラベルの数をカウントし、サンプル数の少ないラベルから順に、分割するようにしました。
# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) def getLabel(dataList): labels = {} for data in dataList: for annotation in data.annotations: label = annotation["label"] if(label in labels): labels[label] += 1 else: labels[label] = 1 # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) labels = sorted(labels.items(), key=lambda x:x[1]) return labels labels = getLabel(dataList) # ラベルの数の少ないものから優先して分割する for i,label in enumerate(labels): # ・・・分割処理
5 コード
RecordIOを生成するコードは、以下のとおりです。以下を定義を設定して利用可能です。
- inputPath 元データ(画像データとoutput.manifestを置く)
- outputPath 出力先
- ratio 学習データと検証データの分割比率
内部で使用しているため、im2rec.pyをカレントに配置する必要があります。
import json import os import subprocess # 定義 inputPath = '/tmp/AHIRU-DOG' outputPath = '/tmp/AHIRU-DOG-SageMaker' manifest = 'output.manifest' # 分割の比率 ratio = 0.8 # 8:2に分割する # 1件のデータを表現するクラス class Data(): def __init__(self, src): # プロジェクト名の取得 for key in src.keys(): index = key.rfind("-metadata") if(index!=-1): projectName = key[0:index] # メタデータの取得 metadata = src[projectName + '-metadata'] class_map = metadata["class-map"] # 画像名の取得 self.__imgFileName = os.path.basename(src["source-ref"]) # 画像サイズの取得 project = src[projectName] image_size = project["image_size"] self.__img_width = image_size[0]["width"] self.__img_height = image_size[0]["height"] self.__annotations = [] # アノテーションの取得 for annotation in project["annotations"]: class_id = annotation["class_id"] top = annotation["top"] left = annotation["left"] width = annotation["width"] height = annotation["height"] self.__annotations.append({ "label": class_map[str(class_id)], "width": width, "top": top, "height": height, "left": left }) @property def annotations(self): return self.__annotations # 指定されたラベルを含むかどうか def exsists(self, label): for annotation in self.__annotations: if(annotation["label"] == label): return True return False # .lstを生成して追加する def appendLst(self, lst, labels): cls_list = [] for label in labels: cls_list.append(label[0]) index = len(lst.split('\n')) headerSize = 4 # hederSize,dataSize,imageWidth,imageHeight dataSize = 5 str = "{}\t{}\t{}\t{}\t{}".format(index, headerSize, dataSize, self.__img_width, self.__img_height) for annotation in self.__annotations: cls_id = cls_list.index(annotation["label"]) left = annotation["left"] right = left + annotation["width"] top = annotation["top"] bottom = top + annotation["height"] left = round(left / self.__img_width, 3) right = round(right / self.__img_width, 3) top = round(top / self.__img_height, 3) bottom = round(bottom / self.__img_height, 3) str += "\t{}\t{}\t{}\t{}\t{}".format(cls_id, left, top, right, bottom) fileName = self.__imgFileName str += "\t{}".format(fileName) lst += str + "\n" return lst # dataListをラベルを含むものと、含まないものに分割する def deviedDataList(dataList, label): targetList = [] unTargetList = [] for data in dataList: if(data.exsists(label)): targetList.append(data) else: unTargetList.append(data) return (targetList, unTargetList) # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) def getLabel(dataList): labels = {} for data in dataList: for annotation in data.annotations: label = annotation["label"] if(label in labels): labels[label] += 1 else: labels[label] = 1 # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) labels = sorted(labels.items(), key=lambda x:x[1]) return labels # 全てのJSONデータを読み込む def getDataList(inputPath, manifest): dataList = [] with open("{}/{}".format(inputPath, manifest), 'r') as f: srcList = f.read().split('\n') for src in srcList: if(src != ''): json_src = json.loads(src) dataList.append(Data(json.loads(src))) return dataList def main(): # 出力先フォルダ生成 os.makedirs(outputPath, exist_ok=True) # 全てのJSONデータを読み込む dataList = getDataList(inputPath, manifest) log = "全データ: {}件 ".format(len(dataList)) # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) labels = getLabel(dataList) for i,label in enumerate(labels): log += "[{}]{}: {}件 ".format(i, label[0], label[1]) print(log) # 保存済みリスト storedList = [] log = '' # 学習数 trainCount = 0 # .lst形式 train = '' validation = '' # ラベルの数の少ないものから優先して分割する for i,label in enumerate(labels): log = '' log += "{} => ".format(label[0]) # dataListをラベルが含まれるものと、含まないものに分割する (targetList, unTargetList) = deviedDataList(dataList, label[0]) # 保存済みリストから、当該ラベルで既に保存済の件数をカウントする (include, notInclude) = deviedDataList(storedList, label[0]) storedCounst = len(include) # train用に必要な件数 count = int(label[1] * ratio) - storedCounst log += "{}:".format(count) # train側への保存 for i in range(count): data = targetList.pop() train = data.appendLst(train, labels) trainCount+=1 storedList.append(data) # validation側への保存 log += "{} ".format(len(targetList)) for data in targetList: validation = data.appendLst(validation, labels) storedList.append(data) dataList = unTargetList log += "残り:{}件".format(len(dataList)) print(log) print("Train: {}件".format(trainCount)) # .lstファイルの生成 trainLst = "{}/train.lst".format(outputPath) validationLst = "{}/validation.lst".format(outputPath) with open(trainLst, mode='w') as f: f.write(train) with open(validationLst, mode='w') as f: f.write(validation) # im2rec.pyによるRecordIOファイル生成 # python im2rec.py --pack-label im2rec = "{}/im2rec.py".format(os.getcwd()) cmd = ["python3", im2rec, "--pack-label", "validation.lst", inputPath] result = subprocess.run(cmd, cwd=outputPath) print(result) cmd = ["python3", im2rec, "--pack-label", "train.lst", inputPath] result = subprocess.run(cmd, cwd=outputPath) print(result) main()
例えば、画像ファイルが50件で、ラベルDOGが10件、ラベルAHIRUが50件アノテーションされているデータをを変換すると、以下のようなログが出力されます。
全データ: 50件 [0]DOG: 10件 [1]AHIRU: 50件 DOG => 8:2 残り:40件 AHIRU => 30:10 残り:0件 Train: 38件
そして、出力先に指定したディレクトリで、RecordIOファイル(train.rec及び、varidation.rec)が生成されていることを確認できます。 この2つのファイルを、S3にアップロードして使用します。
$ ls -la total 12632 drwxr-xr-x 8 sin staff 256 4 26 15:12 . drwxr-xr-x 20 sin staff 640 4 26 13:29 .. -rw-r--r-- 1 sin staff 395 4 26 15:12 train.idx -rw-r--r-- 1 sin staff 2114 4 26 15:12 train.lst -rw-r--r-- 1 sin staff 4105152 4 26 15:12 train.rec -rw-r--r-- 1 sin staff 108 4 26 15:12 varidation.idx -rw-r--r-- 1 sin staff 652 4 26 15:12 varidation.lst -rw-r--r-- 1 sin staff 1297368 4 26 15:12 varidation.rec
6 オブジェクト検出
非常に限られたサンプル(データ)数ですが、とりあえず、モデルを作成してみました。DOGは、10件という事で、無理ですが、AHIRUの方は、限定的ですが検出できている感じがします。
7 最後に
今回は、Ground Truthで作成されたデータをSageMakerで利用するためのデータに変換する作業を行ってみました。
Ground Truthのデータを中心とした、データセット作成の作業では、今回のものが、図中の③になっています。